package plugins.script;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Vector;
import java.util.logging.Logger;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.DefaultListModel;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.ListSelectionModel;
import javax.swing.WindowConstants;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;

import plugins.script.ScriptingEngine.ErrorHandler;
import freemind.controller.BlindIcon;
import freemind.controller.MenuItemEnabledListener;
import freemind.controller.StructuredMenuHolder;
import freemind.controller.actions.generated.instance.ScriptEditorWindowConfigurationStorage;
import freemind.main.FreeMindMain;
import freemind.main.Resources;
import freemind.main.Tools;

public class ScriptEditorPanel extends JDialog implements MenuListener {
	private static final long serialVersionUID = 3221975191441136520L;
	private static final String WINDOW_PREFERENCE_STORAGE_PROPERTY = "plugins.script.ScriptEditorPanel/window_positions";
	private final FreeMindMain mFrame;
	private final ScriptModel mScriptModel;
	private JList<String> mScriptList;
	private JTextArea mScriptTextField;
	private DefaultListModel<String> mListModel;
	private Integer mLastSelected = null;
	private JTextArea mScriptResultField;
	private JSplitPane mCentralUpperPanel;
	private JSplitPane mCentralPanel;
	private Logger logger;
	private JLabel mStatus;
	private AbstractAction mRunAction;
    private StopAction mStopAction;
    private SignAction mSignAction;
	private Thread mScriptThread;
	private Vector<AbstractAction> mMenuActions = new Vector<>();

	private final class ResultFieldStream extends OutputStream {
		public void write(int pByte) throws IOException {
			write(new byte[] { (byte) pByte });
		}

		public void write(final byte[] pB) throws IOException {
			EventQueue.invokeLater(() -> mScriptResultField.append(new String(pB)));
		}
	}

	private final class RunAction extends AbstractAction implements
			MenuItemEnabledListener {
		private RunAction(String pArg0) {
			super(pArg0);
		}

		public void actionPerformed(ActionEvent arg0) {
			storeCurrent();
			if (isSomethingSelected()) {
				mScriptResultField.setText("");
				mScriptThread = new Thread(() -> {
                    mScriptModel.executeScript(
                            mScriptList.getSelectedIndex(),
                            getPrintStream(), getErrorHandler());
                });
				mScriptThread.start();
			}
		}

		@Override
		public boolean isEnabled(JMenuItem pItem, Action pAction) {
			return isSomethingSelected()
					&& (mScriptThread == null || !mScriptThread.isAlive());
		}
	}

	private final class StopAction extends AbstractAction implements
			MenuItemEnabledListener {
		private StopAction(String pArg0) {
			super(pArg0);
		}

		public void actionPerformed(ActionEvent arg0) {
			if (isEnabled(null, this)) {
				interruptScript();
			}
		}

		@Override
		public boolean isEnabled(JMenuItem pItem, Action pAction) {
			return isSomethingSelected() && mScriptThread != null
					&& !mScriptThread.isInterrupted();
		}
	}

	private final class SignAction extends AbstractAction {
		private SignAction(String pArg0) {
			super(pArg0);
		}

		public void actionPerformed(ActionEvent arg0) {
			storeCurrent();
			if (!mScriptList.isSelectionEmpty()) {
				int selectedIndex = mScriptList.getSelectedIndex();
				ScriptHolder script = mScriptModel.getScript(selectedIndex);
				String signedScript = new SignedScriptHandler().signScript(script.mScript, Resources.getInstance(), mFrame);
				script.setScript(signedScript);
				mScriptModel.setScript(selectedIndex, script);
				mScriptTextField.setText(signedScript);
			}
		}
	}

	private final class CancelAction extends AbstractAction {
		private CancelAction(String pArg0) {
			super(pArg0);
		}

		public void actionPerformed(ActionEvent arg0) {
			disposeDialog(true);
		}
	}

	private final class ExitAction extends AbstractAction {
		private ExitAction(String pArg0) {
			super(pArg0);
		}

		public void actionPerformed(ActionEvent arg0) {
			storeCurrent();
			disposeDialog(false);
		}
	}

	private final class NewScriptAction extends AbstractAction {
		private NewScriptAction(String pArg0) {
			super(pArg0);
		}

		public void actionPerformed(ActionEvent arg0) {
			storeCurrent();
			mLastSelected = null;
			int scriptIndex = mScriptModel.addNewScript();
			updateFields();
			select(scriptIndex);
		}
	}

	public static class ScriptHolder {
		String mScript;
		String mScriptName;

		public ScriptHolder(String pScriptName, String pScript) {
			super();
			mScript = pScript;
			mScriptName = pScriptName;
		}

		public String getScript() {
			return mScript;
		}

		String getScriptName() {
			return mScriptName;
		}

		public ScriptHolder setScript(String pScript) {
			mScript = pScript;
			return this;
		}

		public ScriptHolder setScriptName(String pScriptName) {
			mScriptName = pScriptName;
			return this;
		}
	}

	public interface ScriptModel {
		int getAmountOfScripts();

		/**
		 * @param pIndex
		 *            zero-based
		 * @return a script
		 */
		ScriptHolder getScript(int pIndex);

		void setScript(int pIndex, ScriptHolder pScript);

		boolean executeScript(int pIndex, PrintStream outStream,
				ErrorHandler pErrorHandler);

		void storeDialogPositions(ScriptEditorPanel pPanel,
				ScriptEditorWindowConfigurationStorage pStorage,
				String pWindow_preference_storage_property);

		ScriptEditorWindowConfigurationStorage decorateDialog(
				ScriptEditorPanel pPanel,
				String pWindow_preference_storage_property);

		void endDialog(boolean pIsCanceled);

		boolean isDirty();

		int addNewScript();
	}

	public ScriptEditorPanel(ScriptModel pScriptModel, FreeMindMain pFrame,
			boolean pHasNewScriptFunctionality) {
		super(pFrame.getJFrame(), true);
		logger = pFrame.getLogger(this.getClass().getName());
		mScriptModel = pScriptModel;
		mFrame = pFrame;

		// build the panel:
		this.setTitle(pFrame.getResourceString("plugins/ScriptEditor/window.title"));
		this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
		this.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent event) {
				disposeDialog(true);
			}
		});
		Tools.addEscapeActionToDialog(this, new AbstractAction() {
			public void actionPerformed(ActionEvent arg0) {
				disposeDialog(true);
			}
		});

		Container contentPane = this.getContentPane();

		contentPane.setLayout(new BorderLayout());
		mListModel = new DefaultListModel<>();
		mScriptList = new JList<>(mListModel);
		mScriptList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		mScriptList.addListSelectionListener(pEvent -> {
            if (pEvent.getValueIsAdjusting())
                return;
            select(mScriptList.getSelectedIndex());
        });
		mScriptTextField = new JTextArea();
		mScriptTextField.setFont(new Font("Monospaced", Font.PLAIN, 12));
		mScriptTextField.setEnabled(false);
		mScriptTextField.setTabSize(2);
		mCentralUpperPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, mScriptList, new JScrollPane(mScriptTextField));
		mCentralUpperPanel.setContinuousLayout(true);
		mScriptResultField = new JTextArea();
		mScriptResultField.setEditable(false);
		mScriptResultField.setWrapStyleWord(true);
		mCentralPanel = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
				mCentralUpperPanel, new JScrollPane(mScriptResultField));
		mCentralPanel.setDividerLocation(0.8);
		mCentralPanel.setContinuousLayout(true);
		contentPane.add(mCentralPanel, BorderLayout.CENTER);
		mStatus = new JLabel();
		contentPane.add(mStatus, BorderLayout.SOUTH);
		mScriptTextField.addCaretListener(arg0 -> {
            int caretPosition = mScriptTextField.getCaretPosition();
            try {
                int lineOfOffset = mScriptTextField
                        .getLineOfOffset(caretPosition);
                mStatus.setText("Line: "
                        + (lineOfOffset + 1)
                        + ", Column: "
                        + (caretPosition
                                - mScriptTextField
                                        .getLineStartOffset(lineOfOffset) + 1));
            } catch (BadLocationException e) {
                Resources.getInstance().logException(e);
            }

        });
		updateFields();
		mScriptTextField.repaint();

		// menu:
		JMenuBar menuBar = new JMenuBar();
		JMenu menu = new JMenu();
		menu.addMenuListener(this);
		Tools.setLabelAndMnemonic(menu,
				pFrame.getResourceString("plugins/ScriptEditor.menu_actions"));
		if (pHasNewScriptFunctionality) {
			addAction(
					menu,
					new NewScriptAction(
							pFrame.getResourceString("plugins/ScriptEditor.new_script")));
		}
		mRunAction = new RunAction(pFrame.getResourceString("plugins/ScriptEditor.run"));
		mRunAction.setEnabled(false);
		addAction(menu, mRunAction);
		mStopAction = new StopAction(pFrame.getResourceString("plugins/ScriptEditor.stop"));
		mStopAction.setEnabled(false);
		addAction(menu, mStopAction);
		mSignAction = new SignAction(pFrame.getResourceString("plugins/ScriptEditor.sign"));
		mSignAction.setEnabled(false);
		addAction(menu, mSignAction);
		AbstractAction cancelAction = new CancelAction(pFrame.getResourceString("plugins/ScriptEditor.cancel"));
		addAction(menu, cancelAction);
		AbstractAction exitAction = new ExitAction(pFrame.getResourceString("plugins/ScriptEditor.exit"));
		addAction(menu, exitAction);
		menuBar.add(menu);
		this.setJMenuBar(menuBar);

		// Retrieve window size and column positions.
		ScriptEditorWindowConfigurationStorage storage = mScriptModel
				.decorateDialog(this, WINDOW_PREFERENCE_STORAGE_PROPERTY);
		if (storage != null) {
			mCentralUpperPanel.setDividerLocation(storage.getLeftRatio());
			mCentralPanel.setDividerLocation(storage.getTopRatio());
		} else {
			mCentralUpperPanel.setDividerLocation(100);
			mCentralPanel.setDividerLocation(240);
		}

	}

	private void addAction(JMenu menu, AbstractAction action) {
		mMenuActions.add(action);
		JMenuItem item = menu.add(action);
		Tools.setLabelAndMnemonic(item,
				(String) action.getValue(AbstractAction.NAME));
		item.setIcon(new BlindIcon(StructuredMenuHolder.ICON_SIZE));
	}

	private void updateFields() {
		mListModel.clear();
		for (int i = 0; i < mScriptModel.getAmountOfScripts(); ++i) {
			ScriptHolder script = mScriptModel.getScript(i);
			mListModel.addElement(script.getScriptName());
		}
	}

	private void select(int pIndex) {
		mScriptTextField.setEnabled(pIndex >= 0);
		mSignAction.setEnabled(pIndex >= 0);
		if (pIndex < 0) {
			mScriptTextField.setText("");
			return;
		}
		storeCurrent();
		mScriptTextField.setText(mScriptModel.getScript(pIndex).getScript());
		mLastSelected = new Integer(pIndex);
		if (pIndex >= 0 && mScriptList.getSelectedIndex() != pIndex) {
			mScriptList.setSelectedIndex(pIndex);
		}
	}

	private void storeCurrent() {
		if (mLastSelected != null) {
			// store old value:
			int oldIndex = mLastSelected;
			mScriptModel.setScript(oldIndex, mScriptModel.getScript(oldIndex).setScript(mScriptTextField.getText()));
		}
	}

	private void disposeDialog(boolean pIsCanceled) {
		// the script should be stopped.
		interruptScript();
		// store current script:
		if (!mScriptList.isSelectionEmpty()) {
			select(mScriptList.getSelectedIndex());
		}
		if (pIsCanceled && mScriptModel.isDirty()) {
			// ask if really cancel:
			int action = JOptionPane.showConfirmDialog(this, mFrame.getResourceString("ScriptEditorPanel.changed_save"), "FreeMind", JOptionPane.YES_NO_CANCEL_OPTION);
			if (action == JOptionPane.CANCEL_OPTION)
				return;
			if (action == JOptionPane.YES_OPTION) {
				pIsCanceled = false;
			}
		}

		// store window positions:
		ScriptEditorWindowConfigurationStorage storage = new ScriptEditorWindowConfigurationStorage();
		storage.setLeftRatio(mCentralUpperPanel.getDividerLocation());
		storage.setTopRatio(mCentralPanel.getDividerLocation());
		mScriptModel.storeDialogPositions(this, storage, WINDOW_PREFERENCE_STORAGE_PROPERTY);
		this.setVisible(false);
		this.dispose();
		mScriptModel.endDialog(pIsCanceled);
	}

	PrintStream getPrintStream() {
		return new PrintStream(new ResultFieldStream());
	}

	ErrorHandler getErrorHandler() {
		return pLineNumber -> {
            logger.info("Line number: " + pLineNumber);
            EventQueue.invokeLater(() -> {
                if (pLineNumber > 0
                        && pLineNumber <= mScriptTextField
                                .getLineCount()) {
                    Element element3 = mScriptTextField.getDocument()
                            .getDefaultRootElement();
                    Element element4 = element3
                            .getElement(pLineNumber - 1);
                    if (element4 != null) {
                        mScriptTextField.select(
                                element4.getStartOffset(),
                                element4.getEndOffset());
                    }
                }
            });
        };
	}

	private void interruptScript() {
		if (mScriptThread != null) {
			mScriptThread.interrupt();
			try {
				mScriptThread.join();
				mScriptThread = null;
			} catch (InterruptedException e) {
				freemind.main.Resources.getInstance().logException(e);
			}
		}
	}

	@Override
	public void menuSelected(MenuEvent pE) {
		mMenuActions.stream().filter(action -> action instanceof MenuItemEnabledListener).forEach(action -> {
			MenuItemEnabledListener listener = (MenuItemEnabledListener) action;
			action.setEnabled(listener.isEnabled(null, action));
		});
	}

	@Override
	public void menuDeselected(MenuEvent pE) {
		// TODO Auto-generated method stub
	}

	@Override
	public void menuCanceled(MenuEvent pE) {
		// TODO Auto-generated method stub
	}

	private boolean isSomethingSelected() {
		return !mScriptList.isSelectionEmpty();
	}
}
